// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: taekwon

#include "coin2/exchange/okex_futures/order_v3/parser.h"

#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/okex/order_v3/error_code.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::okex_futures::order_v3 {

namespace {

using namespace coin2::exchange::okex::order_v3::error_code;

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::CurrencyBalance;
using coin::proto::CurrencyTransferElement;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;
using coin::proto::TransferRequestProto;
using coin2::exchange::base::order::IOrderContext;
using coin2::exchange::okex::symbology::OkexCurrency;
using coin2::exchange::okex_futures::symbology::OkexFuturesProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
using TransferType = coin::proto::TransferRequestProto::TransferType;

// see part response in https://www.okex.com/docs/en/#futures-list
inline coin::proto::OrderSide GetOrderSide(const JsonValue& data) {
  using coin::proto::OrderSide;
  const char* type = data["type"].GetString();
  OrderSide side;

  if (type[0] == '1') {
    side = OrderSide::BUY_OPEN_ORDER;
  } else if (type[0] == '2') {
    side = OrderSide::SELL_OPEN_ORDER;
  } else if (type[0] == '3') {
    side = OrderSide::BUY_CLOSE_ORDER;
  } else if (type[0] == '4') {
    side = OrderSide::SELL_CLOSE_ORDER;
  } else {
    side = OrderSide::UNKNOWN_ORDER_SIDE;
  }
  return side;
}

inline bool IsOkexErrMessage(const JsonValue& doc) {
  if (doc.HasMember("error_code") && doc["error_code"].IsInt() && doc["error_code"].GetInt() > 0 &&
      doc.HasMember("error_message")) {
    return true;
  }
  if (doc.HasMember("error_code") && doc["error_code"].IsString() &&
      std::string_view(doc["error_code"].GetString()) != "0" && doc.HasMember("error_message")) {
    return true;
  }
  return false;
}

inline bool IsSwap(const std::string& instrument_id) {
  return instrument_id.find("SWAP") != std::string::npos;
}

bool IsOrderUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (!(json.HasMember("table") && json.HasMember("data"))) {
    return false;
  }
  std::string table(json["table"].GetString());
  return (table == "futures/order") || (table == "swap/order");
}

}  // namespace

std::map<std::string, TransferType> OkexFuturesParser::futures_transfer_type_map_{
    {"6", TransferRequestProto::DEPOSIT},
    {"7", TransferRequestProto::WITHDRAW},
};

std::map<std::string, TransferType> OkexFuturesParser::perpetual_transfer_type_map_{
    {"5", TransferRequestProto::DEPOSIT},
    {"6", TransferRequestProto::WITHDRAW},
};

void OkexFuturesParser::Prepare() {
  for (auto* prod : symcache_->GetProducts()) {
    if (prod->quote().currency() == "USDT" || prod->quote().currency() == "usdt") {
      usdt_ccys_.insert(prod->base().currency() + "-" + prod->quote().currency());
    }
  }
}

void OkexFuturesParser::SetAccountBalanceProtoFromRaw(
    const JsonValue& data,
    AccountBalance* acc_balance) {
  /*
  {
    "equity": "0.97649821",
    "margin": "0.02272331",
    "realized_pnl": "0.00031104",
    "unrealized_pnl": "-0.00593317",
    "margin_ratio": "4.29734141",
    "margin_mode": "crossed",
    "total_avail_balance": "0.98212034",
    "margin_frozen": "0.02272331",
    "margin_for_unfilled": "0",
    "liqui_mode": "tier",
    "maint_margin_ratio": "0.01",
    "liqui_fee_rate": "0.0002",
    "can_withdraw": "0.9537749",
    "underlying": "ETH-USD",
    "currency": "ETH"
  }
  */
  if (!data.HasMember("margin_mode") || !data.HasMember("currency") || !data.HasMember("equity") ||
      !data.HasMember("unrealized_pnl") || !data.HasMember("margin") ||
      data["margin_mode"] != "crossed") {
    THROW() << "Parse error in account balance" << JsonToString(data);
  }
  if (!symcache_->has_currency_native(data["currency"].GetString())) {
    return;
  }
  auto* currency = symcache_->GetCurrencyByNative(data["currency"].GetString());
  auto* currency_balance = acc_balance->add_each_balance();
  currency_balance->set_currency(currency->currency());
  // "underlying": "BTC-USD", "currency": "BTC"
  if (currency->currency() == "USDT") {
    currency_balance->set_currency_native(data["underlying"].GetString());
  } else {
    currency_balance->set_currency_native(currency->native_currency());
  }
  double total = stod(data["equity"].GetString());
  double hold = stod(data["margin"].GetString());
  double available = total - hold;
  currency_balance->set_total(total);
  currency_balance->set_hold(hold);
  currency_balance->set_available(available);
}

AccountBalance OkexFuturesParser::UpdateUsdtBalance(const AccountBalance& acc_balance) {
  if (!is_prepared_) {
    Prepare();
    is_prepared_ = true;
  }
  ignore_balance_event_ = false;
  if (usdt_ccys_.empty()) {
    return acc_balance;
  }
  AccountBalance acc_balance2;
  acc_balance2.set_exchange("Okex");
  acc_balance2.set_market_type("Futures");
  for (const auto& balance : acc_balance.each_balance()) {
    if (balance.currency() == "USDT" || balance.currency() == "usdt") {
      usdt_bals_[balance.currency_native()] = balance;
      usdt_bals_[balance.currency_native()].set_currency_native(balance.currency_native());
    } else {
      *acc_balance2.add_each_balance() = balance;
    }
  }
  if (usdt_bals_.size() < usdt_ccys_.size()) {
    ignore_balance_event_ = true;
  }
  CurrencyBalance usdt_bal;
  bool found_usdt_balance = false;
  for (const auto& [currency_pair, balance] : usdt_bals_) {
    (void)currency_pair;
    if (!symcache_->has_currency_native(balance.currency())) {
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(balance.currency());
    usdt_bal.set_currency(balance.currency());  // CHECK
    usdt_bal.set_currency_native(currency->native_currency());
    usdt_bal.set_total(usdt_bal.total() + balance.total());
    usdt_bal.set_available(usdt_bal.available() + balance.available());
    usdt_bal.set_hold(usdt_bal.total() + balance.hold());
    found_usdt_balance = true;
  }
  if (found_usdt_balance) {
    *acc_balance2.add_each_balance() = usdt_bal;
  }
  return acc_balance2;
}

void OkexFuturesParser::SetAccountPositionProtoFromRaw(
    const rapidjson::GenericValue<rapidjson::UTF8<>>& data,
    AccountPosition* acc_position) {
  if (!data.HasMember("instrument_id") || !data.HasMember("realised_pnl") ||
      !data.HasMember("long_qty") || !data.HasMember("short_qty") ||
      !data.HasMember("long_avg_cost") || !data.HasMember("short_avg_cost") ||
      !data.HasMember("created_at")) {
    THROW() << "Parse error in account position";
  }
  auto* position = acc_position->add_each_position();
  position->set_native_symbol(data["instrument_id"].GetString());

  double long_position = stod(data["long_qty"].GetString());
  double short_position = -stod(data["short_qty"].GetString());
  double net_position = long_position + short_position;
  double long_avg_cost = stod(data["long_avg_cost"].GetString());
  double short_avg_cost = stod(data["short_avg_cost"].GetString());
  double realized_pnl = stod(data["realised_pnl"].GetString());

  position->set_long_position(long_position);
  position->set_short_position(short_position);
  position->set_net_position(net_position);
  position->set_long_avg_entry_price(long_avg_cost);
  position->set_short_avg_entry_price(short_avg_cost);
  position->set_realized_pnl(realized_pnl);

  double long_avail_position = stod(data["long_avail_qty"].GetString());
  double short_avail_position = -stod(data["short_avail_qty"].GetString());
  position->set_available_long_position(long_avail_position);
  position->set_available_short_position(short_avail_position);
}

void OkexFuturesParser::SetSwapAccountPositionProtoFromRaw(
    const JsonValue& data,
    const std::string& instrument_id,
    AccountPosition* acc_position) {
  /*
    {
      "avail_position": "0",
      "avg_cost": "0",
      "instrument_id": "ETH-USD-SWAP",
      "last": "133.34",
      "leverage": "0",
      "liquidation_price": "0",
      "maint_margin_ratio": "0",
      "margin": "0",
      "position": "0",
      "realized_pnl": "0",
      "settled_pnl": "0",
      "settlement_price": "0",
      "side": "long",
      "timestamp": "2020-03-31T02:17:53.469Z",
      "unrealized_pnl": "0"
    },
    {
      "avail_position": "0",
      "avg_cost": "0",
      "instrument_id": "ETH-USD-SWAP",
      "last": "133.34",
      "leverage": "0",
      "liquidation_price": "0",
      "maint_margin_ratio": "0",
      "margin": "0",
      "position": "0",
      "realized_pnl": "0",
      "settled_pnl": "0",
      "settlement_price": "0",
      "side": "short",
      "timestamp": "2020-03-31T02:17:53.469Z",
      "unrealized_pnl": "0"
    }
  */

  if (!data.HasMember("realized_pnl") || !data.HasMember("position") || !data.HasMember("side") ||
      !data.HasMember("timestamp") || !data.HasMember("avail_position")) {
    THROW() << "Parse error in account position";
  }

  std::string symbol = instrument_id;
  if (data.HasMember("instrument_id")) {
    symbol = std::string(data["instrument_id"].GetString());
  }

  std::string_view side(data["side"].GetString());
  CHECK_THROW(side == "long" || side == "short") << JsonToString(data);

  coin::proto::ProductPosition* position = nullptr;
  for (coin::proto::ProductPosition& pos : *(acc_position->mutable_each_position())) {
    if (pos.native_symbol() == symbol) {
      position = &pos;
    }
  }

  double cost = stod(data["avg_cost"].GetString());

  if (position == nullptr) {
    position = acc_position->add_each_position();
    position->set_native_symbol(symbol);
  }

  if (side == "long") {
    double long_position = stod(data["position"].GetString());
    double long_avail_position = stod(data["avail_position"].GetString());
    position->set_long_position(long_position);
    position->set_available_long_position(long_avail_position);
    position->set_long_avg_entry_price(cost);
  } else {
    double short_position = -stod(data["position"].GetString());
    double short_avail_position = -stod(data["avail_position"].GetString());
    position->set_short_position(short_position);
    position->set_available_short_position(short_avail_position);
    position->set_short_avg_entry_price(cost);
  }

  double realized_pnl = stod(data["realized_pnl"].GetString());
  position->set_net_position(position->long_position() + position->short_position());
  position->set_realized_pnl(realized_pnl + position->realized_pnl());
}

AccountBalance OkexFuturesParser::ParseProductBalance(const JsonValue& doc) {
  /*
  {
    "equity": "0",
    "margin": "0",
    "realized_pnl": "0",
    "unrealized_pnl": "0",
    "margin_ratio": "10000",
    "margin_mode": "crossed",
    "total_avail_balance": "0",
    "margin_frozen": "0",
    "margin_for_unfilled": "0",
    "liqui_mode": "tier",
    "maint_margin_ratio": "0.005",
    "liqui_fee_rate": "0.0005",
    "can_withdraw": "0",
    "underlying": "BTC-USD",
    "currency": "BTC"
  }
  */
  AccountBalance acc_balance;
  if (IsOkexErrMessage(doc)) {
    LOG(ERROR) << "[ParseRestBalance] " << JsonToString(doc);
    return acc_balance;
  }

  acc_balance.set_exchange("Okex");
  acc_balance.set_market_type("Futures");
  if (doc.HasMember("info") && doc["info"].IsObject()) {
    auto info = doc["info"].GetObject();
    for (auto iter = info.MemberBegin(); iter != info.MemberEnd(); ++iter) {
      SetAccountBalanceProtoFromRaw(iter->value, &acc_balance);
    }
  } else {
    SetAccountBalanceProtoFromRaw(doc, &acc_balance);
  }

  return acc_balance;
}

AccountBalance OkexFuturesParser::ParseSwapProductBalance(const JsonValue& doc) {
  /*
{
  "info": {
    "currency": "BTC",
    "equity": "0.0159",
    "fixed_balance": "0.0000",
    "instrument_id": "BTC-USD-SWAP",
    "maint_margin_ratio": "0.0050",
    "margin": "0.0092",
    "margin_frozen": "0.0000",
    "margin_mode": "crossed",
    "margin_ratio": "0.1719",
    "max_withdraw": "0.0066",
    "realized_pnl": "0.0000",
    "timestamp": "2020-03-31T08:09:33.692Z",
    "total_avail_balance": "0.0159",
    "underlying": "BTC-USD",
    "unrealized_pnl": "-0.0001"
  }
}
   */
  AccountBalance acc_balance;
  if (IsOkexErrMessage(doc)) {
    LOG(ERROR) << "[ParseRestBalance] " << JsonToString(doc);
    return acc_balance;
  }
  if (!doc.HasMember("info")) {
    LOG(ERROR) << "[ParseRestBalance] Parse error in account balance" << JsonToString(doc);
    return acc_balance;
  }

  acc_balance.set_exchange("Okex");
  acc_balance.set_market_type("Futures");
  if (doc["info"].IsArray()) {
    for (const auto& data : doc["info"].GetArray()) {
      SetAccountBalanceProtoFromRaw(data, &acc_balance);
    }
  } else {
    SetAccountBalanceProtoFromRaw(doc["info"], &acc_balance);
  }

  return acc_balance;
}

AccountPosition OkexFuturesParser::ParseProductPositionFromRest(const JsonValue& doc) {
  AccountPosition acc_position;
  if (!doc.IsObject() || !doc.HasMember("holding")) {
    LOG(ERROR) << "[ParsePositionFromRest] " << JsonToString(doc);
    return acc_position;
  }
  if (!doc["holding"].IsArray()) {
    LOG(ERROR) << "[ParsePositionFromRest] " << JsonToString(doc);
    return acc_position;
  }
  if (IsOkexErrMessage(doc)) {
    LOG(ERROR) << "[ParsePositionFromRest] " << JsonToString(doc);
    return acc_position;
  }

  acc_position.set_exchange("Okex");
  acc_position.set_market_type("Futures");
  for (const auto& data : doc["holding"].GetArray()) {
    if (data.IsArray()) {
      // itrator tokens
      for (const auto& item : data.GetArray()) {
        SetAccountPositionProtoFromRaw(item, &acc_position);
      }
    } else {
      SetAccountPositionProtoFromRaw(data, &acc_position);
    }
  }

  return acc_position;
}

AccountPosition OkexFuturesParser::ParsePositionFromWs(const JsonValue& doc) {
  AccountPosition acc_position;
  if (IsOkexErrMessage(doc)) {
    LOG(ERROR) << "[ParsePositionFromWs] " << JsonToString(doc);
    return acc_position;
  }
  for (const auto& data : doc["data"].GetArray()) {
    SetAccountPositionProtoFromRaw(data, &acc_position);
  }
  return acc_position;
}

AccountPosition OkexFuturesParser::ParseSwapPositionFromWs(const JsonValue& data) {
  /*
{
  "table": "swap/position",
  "data": [
    {
      "holding": [
        {
          "avail_position": "0",
          "avg_cost": "6436.1",
          "last": "6417.0",
          "leverage": "10.00",
          "liquidation_price": "0.0",
          "maint_margin_ratio": "0.0050",
          "margin": "0.0015",
          "position": "1",
          "realized_pnl": "0.0000",
          "settled_pnl": "0.0000",
          "settlement_price": "6436.1",
          "side": "short",
          "timestamp": "2020-03-31T04:39:48.316Z"
        }
      ],
      "instrument_id": "BTC-USD-SWAP",
      "margin_mode": "crossed",
      "timestamp": "2020-03-31T04:39:48.316Z"
    }
  ]
}
   */
  AccountPosition acc_position;
  if (!(data.HasMember("table") && data["table"].IsString())) {
    LOG(ERROR) << "[ParseSwapPositionFromWs]" << JsonToString(data);
    return acc_position;
  }

  if (std::string("swap/position") != data["table"].GetString()) {
    LOG(ERROR) << "[ParseSwapPositionFromWs]" << JsonToString(data);
    return acc_position;
  }

  if (!(data.HasMember("data") && data["data"].IsArray())) {
    LOG(ERROR) << "[ParseSwapPositionFromWs]" << JsonToString(data);
    return acc_position;
  }

  const auto& info_data = data["data"][0];
  if (!(info_data.HasMember("holding") && info_data["holding"].IsArray())) {
    LOG(ERROR) << "[ParseSwapPositionFromWs]" << JsonToString(data);
    return acc_position;
  }

  if (!(info_data.HasMember("instrument_id") && info_data["instrument_id"].IsString())) {
    LOG(ERROR) << "[ParseSwapPositionFromWs]" << JsonToString(data);
    return acc_position;
  }

  std::string instrument_id(info_data["instrument_id"].GetString());
  for (const auto& info : info_data["holding"].GetArray()) {
    SetSwapAccountPositionProtoFromRaw(info, instrument_id, &acc_position);
  }
  return acc_position;
}

AccountPosition OkexFuturesParser::ParseSwapProductPositionFromRest(const JsonValue& data) {
  /*
REST
{
  "margin_mode": "crossed",
  "timestamp": "2020-03-31T02:17:53.469Z",
  "holding": [
    {
      "avail_position": "0",
      "avg_cost": "0",
      "instrument_id": "ETH-USD-SWAP",
      "last": "133.34",
      "leverage": "0",
      "liquidation_price": "0",
      "maint_margin_ratio": "0",
      "margin": "0",
      "position": "0",
      "realized_pnl": "0",
      "settled_pnl": "0",
      "settlement_price": "0",
      "side": "long",
      "timestamp": "2020-03-31T02:17:53.469Z",
      "unrealized_pnl": "0"
    },
    {
      "avail_position": "0",
      "avg_cost": "0",
      "instrument_id": "ETH-USD-SWAP",
      "last": "133.34",
      "leverage": "0",
      "liquidation_price": "0",
      "maint_margin_ratio": "0",
      "margin": "0",
      "position": "0",
      "realized_pnl": "0",
      "settled_pnl": "0",
      "settlement_price": "0",
      "side": "short",
      "timestamp": "2020-03-31T02:17:53.469Z",
      "unrealized_pnl": "0"
    }
  ]
}
 */

  AccountPosition acc_position;
  if (data.IsArray()) {
    acc_position.set_exchange("Okex");
    acc_position.set_market_type("Futures");
    for (const auto& info : data.GetArray()) {
      if (!(info.HasMember("margin_mode") && info["margin_mode"].IsString())) {
        LOG(ERROR) << JsonToString(info);
        continue;
      }
      if (std::string("crossed") != info["margin_mode"].GetString()) {
        LOG(ERROR) << "[ParseSwapPosition]" << JsonToString(info);
        continue;
      }
      if (!(info.HasMember("holding") && info["holding"].IsArray())) {
        LOG(ERROR) << "[ParseSwapPosition]" << JsonToString(info);
        continue;
      }
      for (const auto& item : info["holding"].GetArray()) {
        SetSwapAccountPositionProtoFromRaw(item, {}, &acc_position);
      }
    }
  } else {
    if (!(data.HasMember("margin_mode") && data["margin_mode"].IsString())) {
      LOG(ERROR) << JsonToString(data);
      return acc_position;
    }
    if (std::string("crossed") != data["margin_mode"].GetString()) {
      LOG(ERROR) << "[ParseSwapPosition]" << JsonToString(data);
      return acc_position;
    }
    if (!(data.HasMember("holding") && data["holding"].IsArray())) {
      LOG(ERROR) << "[ParseSwapPosition]" << JsonToString(data);
      return acc_position;
    }
    acc_position.set_exchange("Okex");
    acc_position.set_market_type("Futures");
    for (const auto& info : data["holding"].GetArray()) {
      SetSwapAccountPositionProtoFromRaw(info, {}, &acc_position);
    }
  }

  return acc_position;
}

void OkexFuturesParser::ParseSubmitResponse() {
  /* normal:
   * {
   *   "client_oid":"oktfuture8",
   *   "order_id":"2517062038154240",
   *   "error_code":"0",
   *   "error_message":"",
   *   "result":true
   * }
   * error:
   * {
   *   "error_message":"Risk rate lower than 100% before opening position",
   *   "code":32015,
   *   "error_code":"32015",
   *   "message":"Risk rate lower than 100% before opening position"
   * }
   */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[OkexFuturesSubmitOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[OkexFuturesSubmitOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[OkexFuturesSubmitOrder] Parse Error\n" << http_context->res.body();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("error_code")) {
    auto error_code = GetOrderErrorCode(json["error_code"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[OkexFuturesSubmitOrder] Order failed! " << JsonToString(json);
      resp->set_success(false);
      return;
    }
  }

  CHECK_THROW(json.HasMember("result")) << http_context->res;
  if (!((json["result"].IsString() && std::string_view(json["result"].GetString()) == "true") ||
        json["result"].GetBool() == true)) {
    LOG(ERROR) << "[OkexFuturesSubmitOrder] Parse Error\n" << http_context->res.body();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }
  resp->set_exchange_order_id(json["order_id"].GetString());
  resp->set_success(true);
}

void OkexFuturesParser::ParseCancelResponse() {
  /* normal:
   * {
   *   "error_message": "",
   *   "result": true,
   *   "error_code": "0",
   *   "client_oid": "oktfuture10",
   *   "order_id": "2517514559122432"
   * }
   * {
   *   "error_message": "",
   *   "result": "true",
   *   "error_code": "0",
   *   "order_id": "467060496035946496"
   * }
   *
   * error:???
   */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[OkexFuturesCancelOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[OkexFuturesCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[OkexFuturesCancelOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("error_code")) {
    auto error_code = coin::proto::OrderErrorCode::ORDER_NO_ERROR;
    error_code = GetOrderErrorCode(json["error_code"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[OkexFuturesCancelOrder] Cancel failed! " << JsonToString(json);
      resp->set_success(false);
      return;
    }
  }

  CHECK_THROW(json.HasMember("result")) << http_context->res;
  if (!((json["result"].IsString() && std::string_view(json["result"].GetString()) == "true") ||
        json["result"].GetBool() == true)) {
    LOG(ERROR) << "[OkexFuturesCancelOrder] Parse Error\n" << http_context->res.body();
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }
  resp->set_exchange_order_id(json["order_id"].GetString());
  resp->set_success(true);
}

ProductOrderElement OkexFuturesParser::ParseOrderInfo(const JsonValue& data) {
  /*

  SWAP
  {
    "table": "swap/order",
    "data": [
      {
        "client_oid": "pl1585637486157612841",
        "contract_val": "100",
        "error_code": "0",
        "fee": "0.000000",
        "filled_qty": "0",
        "instrument_id": "BTC-USD-SWAP",
        "last_fill_px": "0",
        "last_fill_qty": "0",
        "last_fill_time": "1970-01-01T00:00:00.000Z",
        "order_id": "467073905811988480",
        "order_type": "1",
        "price": "6451.0",
        "price_avg": "0.0",
        "size": "1",
        "state": "0",
        "status": "0",
        "timestamp": "2020-03-31T06:51:28.498Z",
        "type": "2"
      }
    ]
  }
  */

  ProductOrderElement order;
  order.set_market_type(MarketType::Futures);
  order.set_exchange_type(ExchangeType::Okex);
  if (IsSwap(data["instrument_id"].GetString())) {
    order.set_api_version("v3-swap");
  } else {
    order.set_api_version("v3");
  }
  order.set_exchange_order_id(data["order_id"].GetString());
  order.set_symbol(data["instrument_id"].GetString());
  order.set_price(stod(data["price"].GetString()));
  order.set_qty(std::stoi(data["size"].GetString()));
  std::string proc_order_id(data["client_oid"].GetString());
  if (!proc_order_id.empty()) {
    order.set_proc_order_id(stoll(proc_order_id.substr(2)));
  }
  order.set_side(GetOrderSide(data));
  order.set_order_type(coin::proto::OrderType::LIMIT_ORDER);
  order.set_timestamp(msg_->timestamp);
  CHECK_THROW(data.HasMember("state"));
  const int status = std::stoi(data["state"].GetString());
  // -2:Failed,-1:Canceled,0:Open ,1:Partially Filled, 2:Fully Filled,3:Submitting,4:Canceling
  switch (status) {
    case -2:
    case 3:
    case 4: {
      order.set_is_live(false);
      break;
    }
    case 0:
    case 1: {
      order.set_is_live(true);
      break;
    }
    case -1: {
      order.set_cancel_confirmed(true);
      break;
    }
    case 2: {
      order.set_fully_filled(true);
      break;
    }
    default: {
      LOG(ERROR) << "Unknown order status! " << status;
      break;
    }
  }
  return order;
}

void OkexFuturesParser::ParseRestOrdersInfo() {
  JsonDocument data;
  if (!PrepareJsonDocument(&data)) {
    LOG(ERROR) << "[ParseRestOrdersInfo] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (data.IsObject() && IsOkexErrMessage(data)) {
    LOG(ERROR) << "[ParseRestOrdersInfo] " << JsonToString(data);
    return;
  }
  if (!data.HasMember("order_info")) {
    LOG(ERROR) << "[ParseRestOrdersInfo] Not .order_info in json. " << JsonToString(data);
    return;
  }

  const auto& info_list = data["order_info"];
  if (!info_list.IsArray()) {
    LOG(ERROR) << "[ParseRestOrdersInfo] " << JsonToString(data);
    return;
  }
  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  for (const auto& info : info_list.GetArray()) {
    auto* order = product_order_bundle->add_each_order();
    auto new_order = ParseOrderInfo(info);
    order->MergeFrom(new_order);
    product_order_bundle_found = true;
  }
  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

ProductOrderBundle OkexFuturesParser::ParseWsOrderInfoListV2(const JsonValue& data) {
  /*
{
  "table": "swap/order",
  "data": [
    {
      "client_oid": "pl1585637486157612841",
      "contract_val": "100",
      "error_code": "0",
      "fee": "0.000000",
      "filled_qty": "0",
      "instrument_id": "BTC-USD-SWAP",
      "last_fill_px": "0",
      "last_fill_qty": "0",
      "last_fill_time": "1970-01-01T00:00:00.000Z",
      "order_id": "467073905811988480",
      "order_type": "1",
      "price": "6451.0",
      "price_avg": "0.0",
      "size": "1",
      "state": "0",
      "status": "0",
      "timestamp": "2020-03-31T06:51:28.498Z",
      "type": "2"
    }
  ]
}

   */
  ProductOrderBundle product_order_bundle;
  if (!data.HasMember("data")) {
    LOG(ERROR) << "Not .data in json. " << JsonToString(data);
    return product_order_bundle;
  }

  const auto& info_list = data["data"];
  CHECK_THROW(info_list.IsArray()) << JsonToString(data);
  for (const auto& info : info_list.GetArray()) {
    auto* order = product_order_bundle.add_each_order();
    auto new_order = ParseOrderInfo(info);
    order->MergeFrom(new_order);
  }
  return product_order_bundle;
}

ProductFillBundle OkexFuturesParser::ParseWsFillInfoListV2(const JsonValue& data) {
  ProductFillBundle product_fill_bundle;
  if (!data.HasMember("data")) {
    return product_fill_bundle;
  }

  const auto& info_list = data["data"];
  if (info_list.Size() == 0) {
    return product_fill_bundle;
  }

  for (const auto& info : info_list.GetArray()) {
    int status = std::stoi(info["state"].GetString());
    if (!(status == 1 || status == 2)) {
      continue;
    }
    auto* fill = product_fill_bundle.add_each_fill();
    fill->set_symbol(info["instrument_id"].GetString());
    fill->set_fill_timestamp(msg_->timestamp);
    fill->set_exchange_order_id(info["order_id"].GetString());
    std::string proc_order_id(info["client_oid"].GetString());
    if (!proc_order_id.empty()) {
      fill->set_proc_order_id(stoll(proc_order_id.substr(2)));
    }
    fill->set_price(stod(info["last_fill_px"].GetString()));
    fill->set_qty(std::stoi(info["last_fill_qty"].GetString()));
    fill->set_fill_id(info["last_fill_id"].GetString());
    fill->set_fee(-stod(info["fee"].GetString()));
    if (fill->fee() > 0) {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    } else {
      fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    }
  }
  return product_fill_bundle;
}

void OkexFuturesParser::ParseRestFillInfoList() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (is_swap()) {
    *result_.mutable_fill_list() = ParseSwapRestFillInfoList(document);
  } else {
    *result_.mutable_fill_list() = ParseRestFillInfoListV2(document);
  }
  result_.set_type(MsgType::ORDER_FILL);
}

ProductFillBundle OkexFuturesParser::ParseRestFillInfoListV2(const JsonValue& data) {
  /*
   *
   * swap:
  {
    "trade_id": "467001105340461056",
    "instrument_id": "BTC-USD-SWAP",
    "order_id": "467001069006168064",
    "price": "6433.5",
    "order_qty": "1.0000",
    "fee": "0.000002",
    "timestamp": "2020-03-31T04:26:49.000Z",
    "exec_type": "M",
    "side": "short"
  }
   */

  ProductFillBundle product_fill_bundle;
  if (!data.IsArray()) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(data);
    return product_fill_bundle;
  }
  if (data.Size() == 0) {
    return product_fill_bundle;
  }

  for (const auto& info : data.GetArray()) {
    auto* fill = product_fill_bundle.add_each_fill();
    fill->set_symbol(info["instrument_id"].GetString());
    fill->set_exchange_order_id(info["order_id"].GetString());
    fill->set_price(stod(info["price"].GetString()));
    fill->set_qty(std::stoi(info["order_qty"].GetString()));
    fill->set_fill_id(info["trade_id"].GetString());
    fill->set_fee(-stod(info["fee"].GetString()));
    if (info["exec_type"].GetString()[0] == 'M') {
      fill->set_fill_type(FillType::MAKER_FILL_TYPE);
    } else if (info["exec_type"].GetString()[0] == 'T') {
      fill->set_fill_type(FillType::TAKER_FILL_TYPE);
    } else {
      fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
    }
  }
  return product_fill_bundle;
}

ProductFillBundle OkexFuturesParser::ParseSwapWsFillInfoList(const JsonValue& data) {
  ProductFillBundle product_fill_bundle;
  if (!data.HasMember("data")) {
    return product_fill_bundle;
  }

  const auto& info_list = data["data"];
  if (info_list.Size() == 0) {
    return product_fill_bundle;
  }

  for (const auto& info : info_list.GetArray()) {
    int status = std::stoi(info["state"].GetString());
    if (!(status == 1 || status == 2)) {
      continue;
    }
    coin::proto::ProductFillElement fill;
    fill.set_symbol(info["instrument_id"].GetString());
    fill.set_fill_timestamp(msg_->timestamp);
    fill.set_exchange_order_id(info["order_id"].GetString());
    std::string proc_order_id(info["client_oid"].GetString());
    if (!proc_order_id.empty()) {
      fill.set_proc_order_id(stoll(proc_order_id.substr(2)));
    }
    fill.set_price(stod(info["last_fill_px"].GetString()));
    fill.set_qty(std::stoi(info["last_fill_qty"].GetString()));
    // !!! Note that this fill_id is different from rest query's fill id.
    fill.set_fill_id(info["last_fill_id"].GetString());
    fill.set_fee(-stod(info["fee"].GetString()));
    if (fill.fee() > 0) {
      fill.set_fill_type(FillType::TAKER_FILL_TYPE);
    } else {
      fill.set_fill_type(FillType::MAKER_FILL_TYPE);
    }
    bool is_new = new_fill_detector_.UpdateByRestFill(&fill);
    if (is_new) {
      auto* new_fill = product_fill_bundle.add_each_fill();
      new_fill->MergeFrom(fill);
    }
  }
  return product_fill_bundle;
}

ProductFillBundle OkexFuturesParser::ParseSwapRestFillInfoList(const JsonValue& data) {
  /*
   * swap:
  {
    "trade_id": "467001105340461056",
    "instrument_id": "BTC-USD-SWAP",
    "order_id": "467001069006168064",
    "price": "6433.5",
    "order_qty": "1.0000",
    "fee": "0.000002",
    "timestamp": "2020-03-31T04:26:49.000Z",
    "exec_type": "M",
    "side": "short"
  }
   */

  ProductFillBundle product_fill_bundle;
  if (!data.IsArray()) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(data);
    return product_fill_bundle;
  }
  if (data.Size() == 0) {
    return product_fill_bundle;
  }

  for (const auto& info : data.GetArray()) {
    coin::proto::ProductFillElement fill;
    fill.set_symbol(info["instrument_id"].GetString());
    fill.set_exchange_order_id(info["order_id"].GetString());
    fill.set_price(stod(info["price"].GetString()));
    fill.set_qty(std::stoi(info["order_qty"].GetString()));
    fill.set_fee(-stod(info["fee"].GetString()));
    if (info["exec_type"].GetString()[0] == 'M') {
      fill.set_fill_type(FillType::MAKER_FILL_TYPE);
    } else if (info["exec_type"].GetString()[0] == 'T') {
      fill.set_fill_type(FillType::TAKER_FILL_TYPE);
    } else {
      fill.set_fill_type(FillType::UNKNOWN_FILL_TYPE);
    }
    // !!! Note that this fill_id is different from ws update's fill id.
    fill.set_fill_id(info["trade_id"].GetString());
    bool is_new = new_fill_detector_.UpdateByRestFill(&fill);
    if (is_new) {
      auto* new_fill = product_fill_bundle.add_each_fill();
      new_fill->MergeFrom(fill);
    }
  }
  return product_fill_bundle;
}

void OkexFuturesParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (ignore_balance_event_) {
    result_.Clear();
    result_.set_type(MsgType::IGNORABLE);
    return;
  }
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
auto OkexFuturesParser::ParseBalance(const JsonValue& document) -> AccountBalance {
  if (is_swap()) {
    return UpdateUsdtBalance(ParseSwapProductBalance(document));
  } else {
    return UpdateUsdtBalance(ParseProductBalance(document));
  }
}

void OkexFuturesParser::ParseRestAccountPosition() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountPosition] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_position = ParsePosition(document);
  if (!account_position.has_exchange() || !account_position.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  result_.set_type(MsgType::ACCOUNT_POSITION);
  *result_.mutable_account_position() = std::move(account_position);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
auto OkexFuturesParser::ParsePosition(const JsonValue& document) -> AccountPosition {
  if (is_swap()) {
    return ParseSwapProductPositionFromRest(document);
  } else {
    return ParseProductPositionFromRest(document);
  }
}

void OkexFuturesParser::ParseRestTransfer() {
  /*
  // futures
  [
      {
          "ledger_id":"3786201700599811",
          "timestamp":"2019-10-31T08:00:53.097Z",
          "amount":"0",
          "balance":"0",
          "currency":"btc-usd",
          "underlying":"BTC-USD",
          "type":"settlement",
          "details":{
              "order_id":"0",
              "instrument_id":"BTC-USD-191227"
          }
      }
  ]
  // perpetual
  [
      {
          "amount": "0.000011",
          "balance": "0",
          "fee": "0.000000",
          "currency": "BTC",
          "details": {
              "instrument_id": "BTC-USD-SWAP",
              "order_id": ""
          },
          "type": "funding",
          "instrument_id": "BTC-USD-SWAP",
          "ledger_id": "336890402630811792",
          "timestamp": "2019-10-03T16:00:00.000Z"
      }
  ]
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestTransfer] Parsing Error" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsOkexErrMessage(document)) {
    LOG(ERROR) << "[ParseRestTransfer] Error message" << JsonToString(document);
    return;
  }
  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseRestTransfer] Invalid data format" << JsonToString(document);
    return;
  }

  auto currency_transfer_bundle = result_.mutable_currency_transfer_bundle();
  currency_transfer_bundle->Clear();
  bool currency_transfer_bundle_found = false;
  for (const auto& data : document.GetArray()) {
    if (!data.HasMember("currency") || !data.HasMember("type") || !data.HasMember("amount") ||
        (!data.HasMember("underlying") && !data.HasMember("fee"))) {
      LOG(ERROR) << "[ParseRestTransfer] Invalid transfer format";
      continue;
    }
    if (!symcache_->has_currency_native(data["currency"].GetString())) {
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(data["currency"].GetString());
    auto* currency_transfer = currency_transfer_bundle->add_each_transfer();
    currency_transfer->set_currency(currency->currency());
    std::string type = data["type"].GetString();
    if (is_swap()) {
      if (perpetual_transfer_type_map_.find(type) != perpetual_transfer_type_map_.end()) {
        currency_transfer->set_type(perpetual_transfer_type_map_[type]);
        currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
      }
    } else {
      if (futures_transfer_type_map_.find(type) != futures_transfer_type_map_.end()) {
        currency_transfer->set_type(futures_transfer_type_map_[type]);
        currency_transfer->set_status(TransferRequestProto::TRANSFER_CONFIRMED);
      }
    }
    currency_transfer->set_transfer_id(data["ledger_id"].GetString());
    currency_transfer->set_qty(stod(data["amount"].GetString()));
    currency_transfer->set_create_timestamp(TimestampFromString(data["timestamp"].GetString()));
    if (data.HasMember("fee")) {
      currency_transfer->set_fee(stod(data["fee"].GetString()));
    }
    currency_transfer_bundle_found = true;
  }

  if (currency_transfer_bundle_found) {
    result_.set_type(MsgType::TRANSFER);
  }
}

void OkexFuturesParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  if (is_swap()) {
    if (document.HasMember("table") && document.HasMember("data")) {
      const std::string native_topic(document["table"].GetString());
      if (native_topic == "swap/position") {
        *result_.mutable_account_position() = ParseSwapPositionFromWs(document);
        result_.set_type(MsgType::ACCOUNT_POSITION);
      }
    }
  } else {
    if (document.HasMember("table") && document.HasMember("data")) {
      const std::string native_topic(document["table"].GetString());
      if (native_topic == "futures/position") {
        *result_.mutable_account_position() = ParsePositionFromWs(document);
        result_.set_type(MsgType::ACCOUNT_POSITION);
      }
    }
  }

  if (IsOrderUpdate(document)) {
    if (is_swap()) {
      *result_.mutable_fill_list() = ParseSwapWsFillInfoList(document);
    } else {
      *result_.mutable_fill_list() = ParseWsFillInfoListV2(document);
    }
    *result_.mutable_order_list() = ParseWsOrderInfoListV2(document);
    result_.set_type(MsgType::ORDER_FILL);
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

auto OkexFuturesParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      // pass
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
    case OrderSubTopicId::REST_DONE_ORDERS:
    case OrderSubTopicId::REST_ORDERS_INFO:
      ParseRestOrdersInfo();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case OrderSubTopicId::REST_ACCOUNT_POSITION:
      ParseRestAccountPosition();
      break;
    case OrderSubTopicId::TRANSFER:
      ParseRestTransfer();
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::okex_futures::order_v3
